/*
 * Copyright © 2017 eqxiu.com 北京中网易企秀科技有限公司  All rights reserved.
 */

package cn.hermit.scan;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Classpath scanning and filtering.
 *
 * @author JackGao 高建国
 * @description //TODO 设计说明
 * @date 27/5/2017
 */
public class ClasspathScanner {

    private static final String[] JAVA_SYS_LIB_JARS = {
            "jre/lib/resources.jar",
            "jre/lib/rt.jar",
            "jre/lib/jsse.jar",
            "jre/lib/jce.jar",
            "jre/lib/charsets.jar",
            "jre/lib/jfr.jar",
            "jre/lib/charsets.jar",
            "jre/lib/deploy.jar",
            "jre/lib/ext/dnsns.jar",
            "jre/lib/ext/localedata.jar",
            "jre/lib/ext/sunec.jar",
            "jre/lib/ext/sunjce_provider.jar",
            ".*jre/lib/ext/sunpkcs\\d*\\.jar",
            "jre/lib/ext/zipfs.jar",
            "jre/lib/htmlconverter.jar",
            "jre/lib/javaws.jar",
            "jre/lib/jce.jar",
            "jre/lib/jfr.jar",
            "jre/lib/jfxrt.jar",
            "jre/lib/jsse.jar",
            "jre/lib/management-agent.jar",
            "jre/lib/plugin.jar",
            "jre/lib/resources.jar",
            "jre/lib/rt.jar",
            "lib/ant-javafx.jar",
            "lib/dt.jar",
            "lib/javafx-doclet.jar",
            "lib/javafx-mx.jar",
            "lib/jconsole.jar",
            "lib/sa-jdi.jar",
            "lib/tools.jar",
            "groovyHotSwap/gragent.jar",
            "lib/idea_rt.jar"
    };

    public final String SYS_KEY_CLASSPATH = "hermit.java.class.path";

    private final List<String> messages = new ArrayList<String>();
    private final List<Path> paths = new ArrayList<>();
    private final List<String> classNames = new ArrayList<>();
    private long scanTime = -1;
    private List<ClassLoader> classLoaders = new ArrayList<>();

    private List<ScanFilter> filters = new ArrayList<>();

    private ClasspathScanner() {

        // for java.lang
//        final URL[] theUrls = Launcher.getBootstrapClassPath().getURLs();

//        classLoaders.add(new URLClassLoader(theUrls) {
//            public String toString() {
//                return super.toString() + " [urls=" + Arrays.asList(theUrls) + "]";
//
//            }
//        });

//        addSystemPropertyPaths("java.class.path");

        addSystemPropertyPaths(SYS_KEY_CLASSPATH);
        classLoaders.add(Thread.currentThread().getContextClassLoader());
    }

    /**
     * Same as calling the constructor
     *
     * @return
     */
    public static ClasspathScanner create() {
        return new ClasspathScanner();
    }

    /**
     * Inspects a {@link System}.getProperty by key and registers each path part
     * as on the classpath
     *
     * @param inKey
     * @return
     */
    public ClasspathScanner addSystemPropertyPaths(String inKey) {

        String value = System.getProperty(inKey);
        if (value == null || value.trim().isEmpty()) {
            return this;
        }

        for (String aPath : value.split("" + File.pathSeparatorChar, -1)) {

            if (aPath == null || aPath.trim().isEmpty()) {
                continue;
            }
            final File theFile = new File(aPath);
            if (!theFile.exists()) {
                continue;
            }

            try {

                final URL theUrl = theFile.toURI().toURL();

                classLoaders.add(new URLClassLoader(new URL[]{theUrl}) {

                    public String toString() {

                        return super.toString() + " [url=" + theUrl.toExternalForm() + "]";
                    }

                    ;

                });
            } catch (Exception e) {
                // swallow
            }
        }

        return this;
    }

    /**
     * register a {@link Class} or better its {@link ClassLoader} to ensure to
     * be in the scan.
     *
     * @param inClass
     * @return
     */
    public ClasspathScanner addClass(Class<?>... inClass) {
        if (inClass == null || inClass.length == 0) {
            return this;
        }
        for (Class<?> clz : inClass) {
            addClassLoader(clz.getClassLoader());
        }
        return this;

    }

    public ClasspathScanner addClassLoader(ClassLoader... classLoader) {
        if (classLoader == null || classLoader.length == 0) return this;
        for (ClassLoader theCl : classLoader) {
            while (theCl != null) {

                if (!classLoaders.contains(theCl)) {

                    classLoaders.add(theCl);
                }
                theCl = (ClassLoader) theCl.getParent();
            }
        }
        classLoaders.addAll(Arrays.asList(classLoader));
        return this;
    }

    /**
     * @return the {@link ClassLoader}s included into listing all the
     * {@link URL}s
     */
    public List<ClassLoader> getClassLoaders() {
        return classLoaders;
    }

    /**
     * Actual scanning and filtering of all resources on the listed
     * {@link ClassLoader}s
     *
     * @return
     */
    public ClasspathScanner scan() {

        if (filters == null || filters.size() == 0) {
            messages.add("None ScanFilter setHeader!!!");
            return this;
        }

        final long starttime = System.currentTimeMillis();

        reset();

        for (ClassLoader aClassLoader : classLoaders) {

            if (aClassLoader instanceof URLClassLoader) {

                scanByClassLoader((URLClassLoader) aClassLoader);

            } else {

                messages.add("unsupported ClassLaoder: " + aClassLoader);
            }
        }

        this.scanTime = System.currentTimeMillis() - starttime;

        return this;
    }

    private void reset() {
        classNames.clear();
        paths.clear();
        messages.clear();
    }

    private void scanByClassLoader(URLClassLoader inCl) {

        for (URL aUrl : inCl.getURLs()) {
            if (aUrl.getFile() != null && !aUrl.getFile().isEmpty()) {

                final File theFile = new File(aUrl.getFile());

                if (theFile.exists() && theFile.isDirectory()) {

                    scanByDirectory(inCl, aUrl, theFile);

                } else if (theFile.exists() && theFile.isFile() && theFile.getName().toLowerCase().endsWith(".jar")) {

                    scanByJar(inCl, aUrl, theFile);

                } else {

                    messages.add("not found: " + aUrl);
                }

            }
        }
    }

    /**
     * @return listing of messages during scanning
     */
    public List<String> getMessages() {
        return new ArrayList<>(messages);
    }

    /**
     * @return amount of scanned resources
     */
    public int getResourcesCount() {
        return paths.size();
    }

    public long getScanTime() {
        return scanTime;
    }

    public int getClassesCount() {

        int i = 0;
        for (Path p : paths) {

            i += p.toString().endsWith(".class") ? 1 : 0;
        }

        return i;
    }

    private void scanByJar(final URLClassLoader inCl, URL aUrl, File inJarFile) {

        String aUrlStr = aUrl.toString();
        for (String javaSysLibJar : JAVA_SYS_LIB_JARS) {
            if (javaSysLibJar.startsWith(".*")) {
                if(aUrlStr.matches(javaSysLibJar)) {
                    return;
                }
            } else if (aUrlStr.endsWith(javaSysLibJar)) {
                return;
            }

        }
        try {
            final FileSystem theFs = FileSystems.newFileSystem(Paths.get(aUrl.toURI()), null);

            Files.walkFileTree(theFs.getRootDirectories().iterator().next(), new FileVisitor<Path>() {

                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    String theResourceName = file.toString().substring(1);
                    scanByPath(inCl, theResourceName, file);
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (Exception e) {

            messages.add("failed scanning jar: " + inJarFile + ": " + e);
        }
    }

    private void scanByDirectory(final URLClassLoader inCl, URL aUrl, File inDirFile) {

        try {

            final File rootDir = inDirFile.getCanonicalFile();
            final int rootDirNameLen = rootDir.getCanonicalPath().length();

            Files.walkFileTree(rootDir.toPath(), new FileVisitor<Path>() {

                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {

                    final String theResourceName = file.toFile().getCanonicalPath().substring(rootDirNameLen + 1);
                    scanByPath(inCl, theResourceName, file);
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    return FileVisitResult.CONTINUE;
                }
            });

        } catch (IOException e) {

            throw new RuntimeException(e);
        }
    }

    private void scanByPath(final URLClassLoader classLoader, final String resourceName,
                            final Path path) {
        paths.add(path);
        try {
            for (ScanFilter filter : filters) {
                if (!filter.filter(classLoader, resourceName, path)) {
                    return;
                }
            }
        } catch (Throwable e) {
            messages.add("failed to scan " + resourceName + " : " + e);
            return;
        }
    }

    public ClasspathScanner addFilter(ScanFilter... filter) {
        if (filter == null || filter.length == 0) return this;
        filters.addAll(Arrays.asList(filter));
        return this;
    }

    @Override
    public String toString() {
        return getClass().getName() + " [paths=" + paths.size() + ", scanTime=" + scanTime + "ms, messages="
                + messages.size() + ", classLoaders=" + classLoaders + "]";
    }

    public static void main(String[] args) {
        ClasspathScanner scanner = ClasspathScanner.create().addFilter(new ScanFilter() {
            @Override
            public boolean filter(ClassLoader classLoader, String resourceName, Path path) throws Throwable {
                System.out.println(resourceName);
                return false;
            }
        });
        scanner.scan();
    }

}
